package org.hrqing.authorization.config;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.*;
import org.hrqing.authorization.entity.Client;
import org.hrqing.authorization.repo.*;
import org.hrqing.authorization.security.*;
import org.hrqing.authorization.service.JpaAuthorizationService;
import org.hrqing.authorization.service.JpaClientService;
import org.hrqing.authorization.service.JpaConsentService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
 * @auther Hrqing
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class AuthorizationServerConfig {

    private final UserRepo userRepo;
    private final UserRoleRepo userRoleRepo;
    private final RoleRepo roleRepo;
    private final ClientRepo clientRepo;
    private final AuthorizationRepo authorizationRepo;
    private final ConsentRepo consentRepo;

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = OAuth2AuthorizationServerConfigurer.authorizationServer();

        http.csrf(csrf -> csrf.ignoringRequestMatchers(authorizationServerConfigurer.getEndpointsMatcher()));
        http.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher());
        http.authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
        );

        OAuth2AuthorizationCodeRequestAuthenticationProvider provider = new OAuth2AuthorizationCodeRequestAuthenticationProvider(clientService(), authorizationService(), consentService());
        provider.setAuthorizationCodeGenerator(new RandomAuthorizationCodeGenerator());

        http.with(authorizationServerConfigurer, authorizationServer -> {
            authorizationServer.tokenGenerator(tokenGenerator());
            authorizationServer.authorizationEndpoint(endpoint -> endpoint
                    .authenticationProvider(provider)
            );
        });

        http.exceptionHandling(exceptions -> exceptions
                .defaultAuthenticationEntryPointFor(
                        new LoginUrlAuthenticationEntryPoint("/login"),
                        new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                )
        );

        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/login/**", "/error/**").permitAll()
                .anyRequest().authenticated()
        );
        http.formLogin(Customizer.withDefaults());
        http.userDetailsService(defaultUserDetailsService());
        http.oauth2ResourceServer(resource -> resource
                .opaqueToken(token -> token.authenticationConverter(new OpaqueTokenAuthenticationConverter() {
                    @Override
                    public Authentication convert(String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) {
                        OAuth2Authorization authorization = authorizationService().findByToken(introspectedToken, OAuth2TokenType.ACCESS_TOKEN);
                        AuthorizationGrantType authorizationGrantType = authorization.getAuthorizationGrantType();
                        Map<String, Object> attributes = new HashMap<>(authenticatedPrincipal.getAttributes());
                        if (authorizationGrantType.equals(AuthorizationGrantType.AUTHORIZATION_CODE)) {
                            SecurityUser userDetails = (SecurityUser) defaultUserDetailsService().loadUserByUsername(authorization.getPrincipalName());
                            userDetails.setScopes(authorization.getAuthorizedScopes());
                            userDetails.setAttributes(attributes);
                            return new BearerTokenAuthentication(userDetails,
                                    new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, introspectedToken, authenticatedPrincipal.getAttribute("iat"), authenticatedPrincipal.getAttribute("exp")),
                                    userDetails.getAuthorities());
                        }
                        RegisteredClient client = clientService().findById(authorization.getRegisteredClientId());
                        if (client != null) {
                            attributes.put("scopes", client.getScopes());
                        }
                        DefaultOAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal(authenticatedPrincipal.getAttribute("client_id"), attributes, new ArrayList<>());
                        return new BearerTokenAuthentication(principal,
                                new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, introspectedToken, authenticatedPrincipal.getAttribute("iat"), authenticatedPrincipal.getAttribute("exp")),
                                new ArrayList<>());
                    }
                }))
        );
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService defaultUserDetailsService() {
        return new JpaUserDetailsService(userRepo, userRoleRepo, roleRepo);
    }

    @Bean
    public JpaClientService clientService() {
        return new JpaClientService(clientRepo);
    }

    @Bean
    public JpaAuthorizationService authorizationService() {
        return new JpaAuthorizationService(authorizationRepo, clientService());
    }

    @Bean
    public JpaConsentService consentService() {
        return new JpaConsentService(consentRepo, clientService());
    }

    @Bean
    public OAuth2TokenGenerator<?> tokenGenerator() {
        return new DelegatingOAuth2TokenGenerator(
                new RandomAccessTokenGenerator(),
                new RandomRefreshTokenGenerator()
        );
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
                .authorizationEndpoint("/oauth/authorize")
                .tokenEndpoint("/oauth/token")
                .tokenRevocationEndpoint("/oauth/revoke")
                .tokenIntrospectionEndpoint("/oauth/introspect")
                .build();
    }
}
